package com.jingxuan.util;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.DoubleSummaryStatistics;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 基于用户搜索与浏览行为的个性化推荐算法。
 *
 * 该算法遵循以下核心流程：
 * 1. 行为数据预处理：过滤无效搜索与浏览记录
 * 2. 行为分类与权重计算：按照高频搜索、深度浏览、重复浏览、关联浏览配置基础权重，并结合时间衰减
 * 3. 兴趣建模：构建关键词、品类、价格、品牌等偏好画像
 * 4. 多维度评分：融合内容推荐、协同过滤、基础评分、新颖性评分
 * 5. 动态权重调整与结果重排序：根据用户活跃度、行为新鲜度及多样性约束优化推荐集合
 */
public final class UserBehaviorAlgorithm {

    /** 时间衰减系数 λ */
    public static final double TIME_DECAY_LAMBDA = 0.15D;

    /** 多样性因子下限 */
    private static final double DIVERSITY_FACTOR_MIN = 0.5D;

    /** 多样性因子上限 */
    private static final double DIVERSITY_FACTOR_MAX = 1.2D;

    /** 关键词合法性校验 */
    private static final Pattern VALID_KEYWORD_PATTERN = Pattern.compile(".*[\\p{L}\\p{N}].*");

    /** 价格分段宽度（元） */
    private static final BigDecimal PRICE_BUCKET_WIDTH = BigDecimal.valueOf(200);

    /** 行为历史列表默认最大长度 */
    private static final int MAX_SEARCH_HISTORY = 200;

    /** 浏览历史最大长度 */
    private static final int MAX_BROWSE_HISTORY = 300;

    private static final double EPS = 1e-8;

    private UserBehaviorAlgorithm() {
    }

    /**
     * 判断搜索关键词是否有效。
     */
    public static boolean isValidKeyword(String keyword) {
        if (keyword == null) {
            return false;
        }
        String trimmed = keyword.trim();
        return trimmed.length() >= 2 && VALID_KEYWORD_PATTERN.matcher(trimmed).matches();
    }

    /**
     * 判断搜索记录是否满足有效条件。
     */
    public static boolean isValidSearch(SearchRecord record) {
        return record != null && record.getUserId() != null && isValidKeyword(record.getKeyword());
    }

    /**
     * 判断浏览记录是否满足有效条件。
     */
    public static boolean isValidBrowse(BrowseRecord record) {
        if (record == null || record.getUserId() == null || record.getProductId() == null) {
            return false;
        }
        double duration = record.getDurationSeconds();
        Integer depth = record.getPageDepth();
        return duration >= 3.0D && depth != null && depth >= 2;
    }

    /**
     * 将搜索记录加入摘要（带上限）。
     */
    public static void appendSearchRecord(UserBehaviorSummary summary, SearchRecord record) {
        if (summary == null || record == null) {
            return;
        }
        summary.ensureSearchRecords();
        summary.getSearchRecords().add(record);
        trimList(summary.getSearchRecords(), MAX_SEARCH_HISTORY);
        summary.setUpdatedAt(LocalDateTime.now());
    }

    /**
     * 将浏览记录加入摘要（带上限）。
     */
    public static void appendBrowseRecord(UserBehaviorSummary summary, BrowseRecord record) {
        if (summary == null || record == null) {
            return;
        }
        summary.ensureBrowseRecords();
        summary.getBrowseRecords().add(record);
        trimList(summary.getBrowseRecords(), MAX_BROWSE_HISTORY);
        summary.setUpdatedAt(LocalDateTime.now());
    }

    private static <T> void trimList(List<T> list, int maxSize) {
        if (list.size() > maxSize) {
            int removeCount = list.size() - maxSize;
            list.subList(0, removeCount).clear();
        }
    }

    /**
     * 构建用户兴趣画像。
     */
    public static UserInterestProfile buildUserInterestProfile(UserBehaviorSummary summary,
                                                               GlobalBehaviorStats globalStats,
                                                               LocalDateTime now) {
        if (summary == null || summary.isEmpty()) {
            return UserInterestProfile.empty();
        }

        List<SearchRecord> validSearches = summary.getSearchRecords().stream()
            .filter(UserBehaviorAlgorithm::isValidSearch)
            .collect(Collectors.toList());

        List<BrowseRecord> validBrowses = summary.getBrowseRecords().stream()
            .filter(UserBehaviorAlgorithm::isValidBrowse)
            .collect(Collectors.toList());

        int effectiveBehaviorCount = validSearches.size() + validBrowses.size();
        if (effectiveBehaviorCount == 0) {
            return UserInterestProfile.empty();
        }

        Map<String, List<SearchRecord>> keywordGroups = validSearches.stream()
            .collect(Collectors.groupingBy(search -> search.getKeyword().trim().toLowerCase(Locale.ROOT)));

        Map<String, List<BrowseRecord>> productGroups = validBrowses.stream()
            .collect(Collectors.groupingBy(BrowseRecord::getProductId));

        Map<String, Double> keywordWeights = new HashMap<>();
        Map<String, Double> categoryAccumulator = new HashMap<>();
        Map<String, Double> brandAccumulator = new HashMap<>();
        List<Double> priceSamples = new ArrayList<>();

        double totalBehaviorWeight = 0D;
        double recentBehaviorWeight = 0D;

        for (Entry<String, List<SearchRecord>> entry : keywordGroups.entrySet()) {
            String keyword = entry.getKey();
            List<SearchRecord> records = entry.getValue();
            int frequency = records.size();
            if (frequency < 1) {
                continue;
            }
            double baseWeight = frequency >= 3 ? 0.35D : 0.15D;
            double weight = 0D;
            for (SearchRecord record : records) {
                double deltaDays = deltaDays(record.getTimestamp(), now);
                double behaviorWeight = calculateBehaviorWeight(baseWeight, deltaDays, frequency);
                weight += behaviorWeight;
                if (deltaDays <= 7D) {
                    recentBehaviorWeight += behaviorWeight;
                }
            }
            totalBehaviorWeight += weight;

            double tf = frequency;
            long keywordUserCount = globalStats != null ? globalStats.getKeywordUserCount(keyword) : 1L;
            long totalUsers = (globalStats != null && globalStats.getTotalUsers() > 0)
                ? globalStats.getTotalUsers() : Math.max(1L, summary.getUserId() != null ? 1L : 0L);
            double idf = Math.log((totalUsers + 1.0) / (keywordUserCount + 1.0));
            double latestDeltaDays = records.stream()
                .map(SearchRecord::getTimestamp)
                .filter(Objects::nonNull)
                .mapToDouble(ts -> deltaDays(ts, now))
                .min()
                .orElse(30D);
            double timeDecay = Math.exp(-TIME_DECAY_LAMBDA * latestDeltaDays);
            double keywordWeight = tf * idf * timeDecay;
            keywordWeights.put(keyword, keywordWeight);
        }

        Map<String, List<BrowseRecord>> categoryGroups = validBrowses.stream()
            .collect(Collectors.groupingBy(record -> Optional.ofNullable(record.getCategory())
                .map(cat -> cat.trim().toLowerCase(Locale.ROOT))
                .orElse("unknown")));

        for (Entry<String, List<BrowseRecord>> entry : productGroups.entrySet()) {
            List<BrowseRecord> records = entry.getValue();
            int count = records.size();
            BrowseRecord mostRecent = records.stream()
                .filter(rec -> rec.getEndTime() != null)
                .max(Comparator.comparing(BrowseRecord::getEndTime))
                .orElse(records.get(records.size() - 1));

            boolean deepBrowse = records.stream()
                .anyMatch(rec -> rec.getDurationSeconds() >= 60D || safeInt(rec.getPageDepth()) >= 4);
            boolean repeatBrowse = count >= 2;

            double baseWeight = deepBrowse ? 0.30D : repeatBrowse ? 0.20D : 0.10D;
            double deltaDays = deltaDays(mostRecent.getEndTime() != null ? mostRecent.getEndTime() : mostRecent.getStartTime(), now);
            double behaviorWeight = calculateBehaviorWeight(baseWeight, deltaDays, count);
            totalBehaviorWeight += behaviorWeight;
            if (deltaDays <= 7D) {
                recentBehaviorWeight += behaviorWeight;
            }

            String category = Optional.ofNullable(mostRecent.getCategory())
                .map(cat -> cat.trim().toLowerCase(Locale.ROOT))
                .orElse("unknown");
            double depthCoefficient = records.stream()
                .mapToDouble(rec -> 0.3D + 0.7D * Math.min(safeInt(rec.getPageCount()) / 8.0D, 1.0D))
                .average()
                .orElse(0.3D);
            categoryAccumulator.merge(category, behaviorWeight * depthCoefficient, Double::sum);

            String brand = Optional.ofNullable(mostRecent.getBrand())
                .map(b -> b.trim().toLowerCase(Locale.ROOT))
                .orElse("unknown");
            brandAccumulator.merge(brand, behaviorWeight, Double::sum);

            DoubleSummaryStatistics priceStats = records.stream()
                .map(BrowseRecord::getPrice)
                .filter(Objects::nonNull)
                .map(BigDecimal::doubleValue)
                .mapToDouble(Double::doubleValue)
                .summaryStatistics();
            if (priceStats.getCount() > 0) {
                priceSamples.add(priceStats.getAverage());
            }
        }

        double categoryWeightSum = categoryAccumulator.values().stream().mapToDouble(Double::doubleValue).sum();
        Map<String, Double> normalizedCategory = categoryAccumulator.entrySet().stream()
            .collect(Collectors.toMap(Entry::getKey,
                entry -> categoryWeightSum > 0 ? entry.getValue() / categoryWeightSum : 0D));

        double brandWeightSum = brandAccumulator.values().stream().mapToDouble(Double::doubleValue).sum();
        Map<String, Double> normalizedBrand = brandAccumulator.entrySet().stream()
            .collect(Collectors.toMap(Entry::getKey,
                entry -> brandWeightSum > 0 ? entry.getValue() / brandWeightSum : 0D));

        DoubleStream priceStream = priceSamples.stream().mapToDouble(Double::doubleValue);
        double averagePrice = priceStream.average().orElse(0D);
        double variance = priceSamples.stream()
            .mapToDouble(price -> Math.pow(price - averagePrice, 2))
            .average()
            .orElse(0D);
        double priceSensitivity = averagePrice > 0 ? Math.max(0D, 1D - (variance / (Math.pow(averagePrice, 2) + EPS))) : 0.5D;

        double personalizationDegree = Math.min(effectiveBehaviorCount / 40.0D, 1.0D);
        double recencyRatio = totalBehaviorWeight > 0 ? recentBehaviorWeight / totalBehaviorWeight : 0D;

        // 估算信誉、热度、时效偏好
        double reputationPreference = estimatePreference(validBrowses, BrowseRecord::getPositiveRate);
        double heatPreference = estimatePreference(validBrowses, BrowseRecord::getHeatValue);
        double timelinessPreference = estimatePreference(validBrowses, record -> record.getTimelinessValue(now));

        return UserInterestProfile.builder()
            .userId(summary.getUserId())
            .keywordWeights(keywordWeights)
            .categoryPreference(normalizedCategory)
            .brandAttention(normalizedBrand)
            .priceAverage(averagePrice)
            .priceVariance(variance)
            .priceSensitivity(priceSensitivity)
            .reputationPreference(reputationPreference)
            .heatPreference(heatPreference)
            .timelinessPreference(timelinessPreference)
            .personalizationDegree(personalizationDegree)
            .recencyRatio(recencyRatio)
            .totalBehaviorWeight(totalBehaviorWeight)
            .behaviorCount(effectiveBehaviorCount)
            .build();
    }

    private static double estimatePreference(List<BrowseRecord> records,
                                              Function<BrowseRecord, Double> extractor) {
        if (records == null || records.isEmpty()) {
            return 0.5D;
        }
        double weightedSum = 0D;
        double totalWeight = 0D;
        for (BrowseRecord record : records) {
            double value = Optional.ofNullable(extractor.apply(record)).orElse(0.5D);
            double weight = Math.max(1D, record.getDurationSeconds() / 10.0D);
            weightedSum += value * weight;
            totalWeight += weight;
        }
        if (totalWeight <= 0) {
            return 0.5D;
        }
        return Math.min(1D, Math.max(0D, weightedSum / totalWeight));
    }

    /**
     * 个性化推荐主流程。
     */
    public static PersonalizedRecommendationResult personalize(UserBehaviorSummary summary,
                                                                List<ProductCandidate> candidates,
                                                                GlobalBehaviorStats globalStats,
                                                                BaseScoreProvider baseScoreProvider,
                                                                LocalDateTime now) {
        if (candidates == null || candidates.isEmpty()) {
            return PersonalizedRecommendationResult.empty();
        }

        UserInterestProfile profile = buildUserInterestProfile(summary, globalStats, now);
        if (profile.isEmptyProfile()) {
            List<ProductRecommendation> baseOnly = candidates.stream()
                .map(candidate -> buildRecommendation(candidate, profile, baseScoreProvider, now))
                .sorted(Comparator.comparing(ProductRecommendation::getFinalScore).reversed())
                .collect(Collectors.toList());
            return PersonalizedRecommendationResult.of(profile, normalizeFinalList(baseOnly));
        }

        List<ProductRecommendation> recommendations = candidates.stream()
            .map(candidate -> buildRecommendation(candidate, profile, baseScoreProvider, now))
            .collect(Collectors.toList());

        List<ProductRecommendation> ranked = reRankWithDiversity(recommendations, profile);
        return PersonalizedRecommendationResult.of(profile, ranked);
    }

    private static ProductRecommendation buildRecommendation(ProductCandidate candidate,
                                                             UserInterestProfile profile,
                                                             BaseScoreProvider baseScoreProvider,
                                                             LocalDateTime now) {
        double contentScore = calculateContentScore(candidate, profile, now);
        double collaborativeScore = calculateCollaborativeScore(candidate, profile);
        double baseScore = baseScoreProvider != null ? baseScoreProvider.getBaseScore(candidate.getProductId()) : candidate.getBaseScore();
        double noveltyScore = calculateNoveltyScore(candidate, profile, now);

        double personalizationDegree = profile.getPersonalizationDegree();
        double alpha = 0.40D * personalizationDegree + 0.20D * (1D - personalizationDegree);
        double beta = 0.25D * personalizationDegree + 0.15D * (1D - personalizationDegree);
        double gamma = 0.25D * personalizationDegree + 0.45D * (1D - personalizationDegree);
        double delta = 0.10D * personalizationDegree + 0.20D * (1D - personalizationDegree);

        if (profile.getRecencyRatio() > 0.7D) {
            alpha = Math.min(0.7D, alpha + 0.05D);
            beta = Math.max(0.05D, beta - 0.05D);
        }

        double weightSum = alpha + beta + gamma + delta;
        if (Math.abs(weightSum - 1D) > EPS) {
            alpha /= weightSum;
            beta /= weightSum;
            gamma /= weightSum;
            delta /= weightSum;
        }

        double finalScore = alpha * contentScore
            + beta * collaborativeScore
            + gamma * baseScore
            + delta * noveltyScore;

        ProductRecommendation recommendation = ProductRecommendation.builder()
            .productId(candidate.getProductId())
            .productName(candidate.getProductName())
            .category(candidate.getCategory())
            .price(candidate.getPrice())
            .contentScore(contentScore)
            .collaborativeScore(collaborativeScore)
            .baseScore(baseScore)
            .noveltyScore(noveltyScore)
            .alpha(alpha)
            .beta(beta)
            .gamma(gamma)
            .delta(delta)
            .rawFinalScore(finalScore)
            .heatPercentile(candidate.getHeatPercentile())
            .build();
        return recommendation;
    }

    private static List<ProductRecommendation> reRankWithDiversity(List<ProductRecommendation> recommendations,
                                                                   UserInterestProfile profile) {
        if (recommendations == null || recommendations.isEmpty()) {
            return Collections.emptyList();
        }

        Map<String, Integer> categoryCount = new HashMap<>();
        Map<String, Integer> priceBucketCount = new HashMap<>();
        List<ProductRecommendation> sorted = recommendations.stream()
            .sorted(Comparator.comparing(ProductRecommendation::getRawFinalScore).reversed())
            .collect(Collectors.toList());

        List<ProductRecommendation> result = new ArrayList<>();
        for (ProductRecommendation recommendation : sorted) {
            String category = Optional.ofNullable(recommendation.getCategory()).orElse("unknown");
            String priceBucket = priceBucket(recommendation.getPrice());

            double categoryProportion = categoryCount.isEmpty() ? 0D
                : categoryCount.getOrDefault(category, 0) / (double) categoryCount.values().stream().mapToInt(Integer::intValue).sum();

            double priceProportion = priceBucketCount.isEmpty() ? 0D
                : priceBucketCount.getOrDefault(priceBucket, 0) / (double) priceBucketCount.values().stream().mapToInt(Integer::intValue).sum();

            double diversityFactor = 1.2D - 0.4D * categoryProportion - 0.3D * priceProportion;
            diversityFactor = Math.max(DIVERSITY_FACTOR_MIN, Math.min(DIVERSITY_FACTOR_MAX, diversityFactor));

            double popularitySuppression = 1.1D - 0.5D * Math.min(recommendation.getHeatPercentile(), 0.8D);
            double seasonBoost = 1D + 0.3D * estimateSeasonMatch(category, LocalDate.now());

            double adjustedScore = recommendation.getRawFinalScore()
                * diversityFactor
                * popularitySuppression
                * seasonBoost;

            recommendation.setDiversityFactor(diversityFactor);
            recommendation.setPopularitySuppression(popularitySuppression);
            recommendation.setSeasonBoost(seasonBoost);
            recommendation.setFinalScore(adjustedScore);

            result.add(recommendation);

            categoryCount.merge(category, 1, Integer::sum);
            priceBucketCount.merge(priceBucket, 1, Integer::sum);
        }

        result.sort(Comparator.comparing(ProductRecommendation::getFinalScore).reversed());
        return result;
    }

    private static List<ProductRecommendation> normalizeFinalList(List<ProductRecommendation> recommendations) {
        if (recommendations == null || recommendations.isEmpty()) {
            return Collections.emptyList();
        }
        double max = recommendations.stream()
            .mapToDouble(ProductRecommendation::getRawFinalScore)
            .max()
            .orElse(1D);
        for (ProductRecommendation recommendation : recommendations) {
            recommendation.setFinalScore(recommendation.getRawFinalScore());
            recommendation.setContentScore(recommendation.getContentScore());
            recommendation.setCollaborativeScore(recommendation.getCollaborativeScore());
            recommendation.setBaseScore(recommendation.getBaseScore());
            recommendation.setNoveltyScore(recommendation.getNoveltyScore());
            if (max > 0) {
                recommendation.setRawFinalScore(recommendation.getRawFinalScore() / max * 100D);
            }
        }
        return recommendations;
    }

    private static double calculateContentScore(ProductCandidate candidate,
                                                UserInterestProfile profile,
                                                LocalDateTime now) {
        double[] productVector = new double[] {
            candidate.getPriceScore(),
            candidate.getReputationScore(),
            candidate.getHeatScore(),
            candidate.getTimelinessScore()
        };

        double[] userVector = new double[] {
            profile.getPriceSensitivity(),
            profile.getReputationPreference(),
            profile.getHeatPreference(),
            profile.getTimelinessPreference()
        };

        double cosine = cosineSimilarity(userVector, productVector);

        Set<String> productTags = new HashSet<>();
        if (candidate.getCategory() != null) {
            productTags.add(candidate.getCategory());
        }
        if (candidate.getKeywordTags() != null) {
            productTags.addAll(candidate.getKeywordTags());
        }
        Set<String> userTags = new HashSet<>();
        userTags.addAll(profile.getTopCategories(5));
        userTags.addAll(profile.getTopKeywords(5));

        double jaccard = jaccardSimilarity(userTags, productTags);
        return 0.6D * cosine * 100D + 0.4D * jaccard * 100D;
    }

    private static double calculateCollaborativeScore(ProductCandidate candidate,
                                                      UserInterestProfile profile) {
        // 多维度协同过滤评分
        
        // 1. 关键词亲和度（TF-IDF加权匹配）
        Set<String> candidateTags = candidate.getKeywordTags() != null
            ? candidate.getKeywordTags() : Collections.emptySet();
        Map<String, Double> keywordWeights = profile.getKeywordWeights();
        double keywordAffinity = 0D;
        double maxKeywordWeight = keywordWeights.values().stream()
            .mapToDouble(Double::doubleValue).max().orElse(1D);
        
        for (String tag : candidateTags) {
            String normalizedTag = tag.toLowerCase(Locale.ROOT);
            // 精确匹配
            if (keywordWeights.containsKey(normalizedTag)) {
                keywordAffinity += keywordWeights.get(normalizedTag) / Math.max(maxKeywordWeight, EPS);
            }
            // 模糊匹配（部分包含）
            for (Entry<String, Double> entry : keywordWeights.entrySet()) {
                if (!entry.getKey().equals(normalizedTag) && 
                    (normalizedTag.contains(entry.getKey()) || entry.getKey().contains(normalizedTag))) {
                    keywordAffinity += 0.5D * entry.getValue() / Math.max(maxKeywordWeight, EPS);
                }
            }
        }
        keywordAffinity = Math.min(1D, keywordAffinity); // 归一化到[0,1]
        
        // 2. 品类偏好匹配（层级加权）
        String category = Optional.ofNullable(candidate.getCategory())
            .map(c -> c.toLowerCase(Locale.ROOT)).orElse("unknown");
        double categoryScore = profile.getCategoryPreference().getOrDefault(category, 0D);
        // 如果品类不在用户偏好中，给予探索性基础分
        if (categoryScore <= 0 && !profile.getCategoryPreference().isEmpty()) {
            categoryScore = 0.1D; // 探索性基础分
        }
        
        // 3. 品牌亲和度
        double brandScore = 0D;
        if (candidate.getProductName() != null) {
            String productNameLower = candidate.getProductName().toLowerCase(Locale.ROOT);
            for (Entry<String, Double> brandEntry : profile.getBrandAttention().entrySet()) {
                if (productNameLower.contains(brandEntry.getKey())) {
                    brandScore = Math.max(brandScore, brandEntry.getValue());
                }
            }
        }
        
        // 4. 价格匹配度（高斯分布）
        double priceMatchScore = 0.5D; // 默认中等匹配
        if (candidate.getPrice() != null && profile.getPriceAverage() > 0) {
            double priceDiff = Math.abs(candidate.getPrice().doubleValue() - profile.getPriceAverage());
            double priceStd = Math.sqrt(profile.getPriceVariance() + EPS);
            // 高斯衰减：价格越接近用户偏好均值，得分越高
            priceMatchScore = Math.exp(-0.5D * Math.pow(priceDiff / Math.max(priceStd, profile.getPriceAverage() * 0.3D), 2));
        }
        
        // 5. 热度与口碑协同
        double reputationMatch = 1D - Math.abs(candidate.getReputationScore() / 100D - profile.getReputationPreference());
        double heatMatch = 1D - Math.abs(candidate.getHeatScore() / 100D - profile.getHeatPreference());
        double qualityMatch = 0.6D * reputationMatch + 0.4D * heatMatch;
        
        // 综合评分（加权组合）
        double personalizedWeight = profile.getPersonalizationDegree();
        double w1 = 0.30D + 0.10D * personalizedWeight; // 关键词权重
        double w2 = 0.25D + 0.05D * personalizedWeight; // 品类权重
        double w3 = 0.15D * personalizedWeight;          // 品牌权重
        double w4 = 0.15D + 0.05D * personalizedWeight; // 价格权重
        double w5 = 0.15D;                               // 质量权重
        
        double totalWeight = w1 + w2 + w3 + w4 + w5;
        double collaborativeScore = (w1 * keywordAffinity + w2 * categoryScore + w3 * brandScore 
            + w4 * priceMatchScore + w5 * qualityMatch) / totalWeight;
        
        return collaborativeScore * 100D;
    }

    private static double calculateNoveltyScore(ProductCandidate candidate,
                                                UserInterestProfile profile,
                                                LocalDateTime now) {
        // 多维度新颖性评分
        
        // 1. 品类新颖性（用户未浏览或低频浏览的品类）
        String category = Optional.ofNullable(candidate.getCategory())
            .map(c -> c.toLowerCase(Locale.ROOT)).orElse("unknown");
        double categoryPreference = profile.getCategoryPreference().getOrDefault(category, 0D);
        // 使用sigmoid函数平滑处理，避免完全未知品类得分过高
        double categoryNovelty = 1D / (1D + Math.exp(5D * (categoryPreference - 0.3D)));
        
        // 2. 品牌新颖性
        double brandNovelty = 1D;
        if (candidate.getProductName() != null) {
            String productNameLower = candidate.getProductName().toLowerCase(Locale.ROOT);
            double maxBrandAttention = 0D;
            for (Entry<String, Double> brandEntry : profile.getBrandAttention().entrySet()) {
                if (productNameLower.contains(brandEntry.getKey())) {
                    maxBrandAttention = Math.max(maxBrandAttention, brandEntry.getValue());
                }
            }
            brandNovelty = 1D - maxBrandAttention * 0.8D; // 保留一定基础新颖性
        }
        
        // 3. 关键词新颖性（商品标签与用户历史关键词的差异度）
        double keywordNovelty = 1D;
        Set<String> candidateTags = candidate.getKeywordTags() != null
            ? candidate.getKeywordTags() : Collections.emptySet();
        if (!candidateTags.isEmpty() && !profile.getKeywordWeights().isEmpty()) {
            Set<String> userKeywords = profile.getKeywordWeights().keySet();
            Set<String> normalizedTags = candidateTags.stream()
                .map(t -> t.toLowerCase(Locale.ROOT))
                .collect(Collectors.toSet());
            // 计算与用户关键词的交集比例
            long matchCount = normalizedTags.stream()
                .filter(tag -> userKeywords.stream().anyMatch(
                    uk -> tag.contains(uk) || uk.contains(tag)))
                .count();
            double overlapRatio = matchCount / (double) normalizedTags.size();
            keywordNovelty = 1D - overlapRatio * 0.7D;
        }
        
        // 4. 价格区间新颖性
        double priceNovelty = 0.5D;
        if (candidate.getPrice() != null && profile.getPriceAverage() > 0) {
            double price = candidate.getPrice().doubleValue();
            double avgPrice = profile.getPriceAverage();
            double priceStd = Math.sqrt(profile.getPriceVariance() + EPS);
            // 价格偏离用户偏好越远，新颖性越高（但过于偏离会降低接受度）
            double zScore = Math.abs(price - avgPrice) / Math.max(priceStd, avgPrice * 0.2D);
            // 使用钟形曲线：适度偏离得分最高
            priceNovelty = Math.exp(-0.5D * Math.pow(zScore - 1D, 2));
        }
        
        // 5. 时间新颖性（新品加成，使用对数衰减）
        double timeNovelty = 0.5D;
        if (candidate.getReleaseTime() != null) {
            long days = Math.max(0, ChronoUnit.DAYS.between(candidate.getReleaseTime(), now));
            // 对数衰减：前7天保持高新颖性，之后缓慢衰减
            if (days <= 7) {
                timeNovelty = 1D;
            } else if (days <= 30) {
                timeNovelty = 0.9D - 0.3D * Math.log(days - 6) / Math.log(24);
            } else if (days <= 90) {
                timeNovelty = 0.6D - 0.3D * Math.log(days - 29) / Math.log(61);
            } else {
                timeNovelty = Math.max(0.1D, 0.3D * Math.exp(-0.01D * (days - 90)));
            }
        }
        
        // 6. 用户探索意愿（基于用户行为多样性）
        double explorationWillingness = calculateExplorationWillingness(profile);
        
        // 综合新颖性评分（加权组合）
        // 根据用户探索意愿调整权重分配
        double w1 = 0.25D + 0.10D * explorationWillingness; // 品类新颖性
        double w2 = 0.15D + 0.05D * explorationWillingness; // 品牌新颖性
        double w3 = 0.15D;                                   // 关键词新颖性
        double w4 = 0.15D;                                   // 价格新颖性
        double w5 = 0.30D - 0.15D * explorationWillingness; // 时间新颖性
        
        double totalWeight = w1 + w2 + w3 + w4 + w5;
        double rawNovelty = (w1 * categoryNovelty + w2 * brandNovelty + w3 * keywordNovelty 
            + w4 * priceNovelty + w5 * timeNovelty) / totalWeight;
        
        // 应用用户接受度调节
        double userAcceptance = 0.4D + 0.6D * explorationWillingness;
        double finalNovelty = rawNovelty * userAcceptance;
        
        // 新颖性惩罚：完全陌生的商品降低推荐概率
        if (categoryPreference <= 0 && brandNovelty >= 0.95D && keywordNovelty >= 0.95D) {
            finalNovelty *= 0.7D; // 过于陌生的商品降权
        }
        
        return finalNovelty * 100D;
    }
    
    /**
     * 计算用户探索意愿
     */
    private static double calculateExplorationWillingness(UserInterestProfile profile) {
        if (profile.isEmptyProfile()) {
            return 0.5D; // 新用户默认中等探索意愿
        }
        
        // 基于品类多样性
        double categoryDiversity = Math.min(1D, profile.getCategoryPreference().size() / 5.0D);
        
        // 基于品牌多样性
        double brandDiversity = Math.min(1D, profile.getBrandAttention().size() / 5.0D);
        
        // 基于行为新鲜度
        double recencyFactor = profile.getRecencyRatio();
        
        // 基于个性化程度（行为越多越了解用户偏好）
        double personalizationFactor = 1D - profile.getPersonalizationDegree() * 0.3D;
        
        // 综合探索意愿
        return 0.3D * categoryDiversity + 0.2D * brandDiversity 
            + 0.3D * recencyFactor + 0.2D * personalizationFactor;
    }

    private static double cosineSimilarity(double[] vec1, double[] vec2) {
        if (vec1.length != vec2.length) {
            throw new IllegalArgumentException("Vector length mismatch");
        }
        double dot = 0D;
        double norm1 = 0D;
        double norm2 = 0D;
        for (int i = 0; i < vec1.length; i++) {
            dot += vec1[i] * vec2[i];
            norm1 += vec1[i] * vec1[i];
            norm2 += vec2[i] * vec2[i];
        }
        if (norm1 == 0 || norm2 == 0) {
            return 0D;
        }
        return dot / (Math.sqrt(norm1) * Math.sqrt(norm2));
    }

    private static double jaccardSimilarity(Set<String> set1, Set<String> set2) {
        if (set1.isEmpty() && set2.isEmpty()) {
            return 1D;
        }
        Set<String> intersection = new HashSet<>(set1);
        intersection.retainAll(set2);
        Set<String> union = new HashSet<>(set1);
        union.addAll(set2);
        if (union.isEmpty()) {
            return 0D;
        }
        return intersection.size() / (double) union.size();
    }

    private static String priceBucket(BigDecimal price) {
        if (price == null) {
            return "unknown";
        }
        BigDecimal bucketIndex = price.divide(PRICE_BUCKET_WIDTH, 0, RoundingMode.DOWN);
        return bucketIndex.toPlainString();
    }

    private static double estimateSeasonMatch(String category, LocalDate date) {
        if (category == null) {
            return 0.0D;
        }
        int month = date.getMonthValue();
        String normalized = category.toLowerCase(Locale.ROOT);
        if (normalized.contains("air") || normalized.contains("冷")) {
            return (month >= 6 && month <= 8) ? 1.0D : 0.2D;
        }
        if (normalized.contains("heater") || normalized.contains("暖")) {
            return (month == 12 || month <= 2) ? 1.0D : 0.2D;
        }
        if (normalized.contains("ice") || normalized.contains("饮品")) {
            return (month >= 5 && month <= 8) ? 0.8D : 0.3D;
        }
        if (normalized.contains("sport") || normalized.contains("运动")) {
            return (month >= 3 && month <= 10) ? 0.7D : 0.4D;
        }
        return 0.5D;
    }

    private static double calculateBehaviorWeight(double baseWeight, double deltaDays, int count) {
        double timeDecay = Math.exp(-TIME_DECAY_LAMBDA * deltaDays);
        return baseWeight * timeDecay * (1D + Math.log(1D + Math.max(1, count)));
    }

    private static double deltaDays(LocalDateTime time, LocalDateTime now) {
        if (time == null || now == null) {
            return 30D;
        }
        Duration duration = Duration.between(time, now);
        return Math.max(0D, duration.toMillis() / (1000D * 60D * 60D * 24D));
    }

    private static int safeInt(Integer value) {
        return value == null ? 0 : value;
    }

    // ----------------------------------------------------------------------
    // 数据结构定义
    // ----------------------------------------------------------------------

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class SearchRecord implements Serializable {
        private static final long serialVersionUID = 1L;

        private Long userId;
        private String keyword;
        private LocalDateTime timestamp;
        private String clickedProductId;
        private boolean clicked;
        private Integer clickRank;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class BrowseRecord implements Serializable {
        private static final long serialVersionUID = 1L;

        private Long userId;
        private String productId;
        private String productName;
        private String category;
        private String brand;
        private BigDecimal price;
        private Double positiveRate;
        private Double heat;
        private LocalDateTime releaseTime;
        private LocalDateTime startTime;
        private LocalDateTime endTime;
        private Integer pageDepth;
        private Integer pageCount;

        public double getDurationSeconds() {
            if (startTime == null || endTime == null) {
                return 0D;
            }
            return Math.max(0D, Duration.between(startTime, endTime).toMillis() / 1000D);
        }

        public Double getHeatValue() {
            return Optional.ofNullable(heat).orElse(0.5D);
        }

        public Double getTimelinessValue(LocalDateTime now) {
            LocalDateTime ref = now != null ? now : LocalDateTime.now();
            if (releaseTime == null) {
                return 0.5D;
            }
            long days = Math.max(0, ChronoUnit.DAYS.between(releaseTime, ref));
            return Math.exp(-0.05D * days);
        }
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @JsonIgnoreProperties(ignoreUnknown = true)
    public static class UserBehaviorSummary implements Serializable {
        private static final long serialVersionUID = 1L;

        private Long userId;
        @Builder.Default
        private List<SearchRecord> searchRecords = new ArrayList<>();
        @Builder.Default
        private List<BrowseRecord> browseRecords = new ArrayList<>();
        private LocalDateTime updatedAt;

        public boolean isEmpty() {
            return (searchRecords == null || searchRecords.isEmpty())
                && (browseRecords == null || browseRecords.isEmpty());
        }

        void ensureSearchRecords() {
            if (searchRecords == null) {
                searchRecords = new ArrayList<>();
            }
        }

        void ensureBrowseRecords() {
            if (browseRecords == null) {
                browseRecords = new ArrayList<>();
            }
        }
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class GlobalBehaviorStats implements Serializable {
        private static final long serialVersionUID = 1L;

        private long totalUsers;
        @Builder.Default
        private Map<String, Long> keywordUserCount = new ConcurrentHashMap<>();
        @Builder.Default
        private Map<String, Long> productUserCount = new ConcurrentHashMap<>();
        @Builder.Default
        private Map<String, Long> categoryUserCount = new ConcurrentHashMap<>();

        public long getKeywordUserCount(String keyword) {
            return keywordUserCount.getOrDefault(keyword, 0L);
        }
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class UserInterestProfile implements Serializable {
        private static final long serialVersionUID = 1L;

        private Long userId;
        @Builder.Default
        private Map<String, Double> keywordWeights = new HashMap<>();
        @Builder.Default
        private Map<String, Double> categoryPreference = new HashMap<>();
        @Builder.Default
        private Map<String, Double> brandAttention = new HashMap<>();
        private double priceAverage;
        private double priceVariance;
        private double priceSensitivity;
        private double reputationPreference;
        private double heatPreference;
        private double timelinessPreference;
        private double personalizationDegree;
        private double recencyRatio;
        private double totalBehaviorWeight;
        private int behaviorCount;

        public static UserInterestProfile empty() {
            return UserInterestProfile.builder()
                .keywordWeights(Collections.emptyMap())
                .categoryPreference(Collections.emptyMap())
                .brandAttention(Collections.emptyMap())
                .priceSensitivity(0.5D)
                .reputationPreference(0.5D)
                .heatPreference(0.5D)
                .timelinessPreference(0.5D)
                .personalizationDegree(0D)
                .recencyRatio(0D)
                .build();
        }

        public boolean isEmptyProfile() {
            return behaviorCount <= 0 || totalBehaviorWeight <= 0;
        }

        public List<String> getTopCategories(int limit) {
            return categoryPreference.entrySet().stream()
                .sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
                .limit(limit)
                .map(Entry::getKey)
                .collect(Collectors.toList());
        }

        public List<String> getTopKeywords(int limit) {
            return keywordWeights.entrySet().stream()
                .sorted((a, b) -> Double.compare(b.getValue(), a.getValue()))
                .limit(limit)
                .map(Entry::getKey)
                .collect(Collectors.toList());
        }

        public Map<String, Double> getKeywordWeights() {
            return keywordWeights != null ? keywordWeights : Collections.emptyMap();
        }

        public Map<String, Double> getCategoryPreference() {
            return categoryPreference != null ? categoryPreference : Collections.emptyMap();
        }
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class ProductCandidate implements Serializable {
        private static final long serialVersionUID = 1L;

        private String productId;
        private String productName;
        private BigDecimal price;
        private double priceScore;
        private double reputationScore;
        private double heatScore;
        private double timelinessScore;
        private Set<String> keywordTags;
        private String category;
        private LocalDateTime releaseTime;
        private double heatPercentile;
        private double baseScore;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class ProductRecommendation implements Serializable {
        private static final long serialVersionUID = 1L;

        private String productId;
        private String productName;
        private String category;
        private BigDecimal price;
        private double contentScore;
        private double collaborativeScore;
        private double baseScore;
        private double noveltyScore;
        private double alpha;
        private double beta;
        private double gamma;
        private double delta;
        private double rawFinalScore;
        private double finalScore;
        private double diversityFactor;
        private double popularitySuppression;
        private double seasonBoost;
        private double heatPercentile;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class PersonalizedRecommendationResult implements Serializable {
        private static final long serialVersionUID = 1L;

        private UserInterestProfile profile;
        @Builder.Default
        private List<ProductRecommendation> recommendations = new ArrayList<>();

        public static PersonalizedRecommendationResult empty() {
            return PersonalizedRecommendationResult.builder()
                .profile(UserInterestProfile.empty())
                .recommendations(Collections.emptyList())
                .build();
        }

        public static PersonalizedRecommendationResult of(UserInterestProfile profile,
                                                           List<ProductRecommendation> recommendations) {
            return PersonalizedRecommendationResult.builder()
                .profile(profile)
                .recommendations(recommendations != null ? recommendations : Collections.emptyList())
                .build();
        }
    }

    @FunctionalInterface
    public interface BaseScoreProvider {
        double getBaseScore(String productId);
    }
}
